Skip to content

✨ Add power modifier to MLIR#1603

Draft
J4MMlE wants to merge 41 commits into
munich-quantum-toolkit:mainfrom
J4MMlE:pow-modifier
Draft

✨ Add power modifier to MLIR#1603
J4MMlE wants to merge 41 commits into
munich-quantum-toolkit:mainfrom
J4MMlE:pow-modifier

Conversation

@J4MMlE

@J4MMlE J4MMlE commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a power modifier (pow) to both MLIR-dialects QC and QCO.
The power modifier raises an operation U by an integer/floating number k pow(k) { U } == U^k in other words the corresponding unitary matrix gets raised by factor k via eigendecomposition: U = V D V† => U^p = V D^p V†.
When k is an integer U^k could also be unrolled ( U is then applied k-times to the input qubit - but this modifier does NOT implement any unrolling - this is left for (possibly) later stages in the pipeline).

QC

qc.pow (2.000000e+00){
        qc.s %q: !qc.qubit
}

QCO

%out = qco.pow (2.000000e+00) (%a = %in) {
        %a_1 = qco.rz(%c_0) %a : !qco.qubit -> !qco.qubit
        qco.yield %a_1
      } : {!qco.qubit} -> {!qco.qubit}

Canonicalizations

The modifier contains the following canonicalizations in both dialects:

  • Nested power flattening: pow(a) { pow(b) { U } } → pow(a*b) { U }
  • Identity power: pow(1) { U } → U
  • Zero power: pow(0) { U } → remove (becomes identity, then eliminated)
  • Negative power: pow(-r) { U } → pow(r) { inv { U } }
  • Canonical ordering: pow { ctrl { U } } → ctrl { pow { U } }
  • Canonical ordering for inv: inv { pow { U } } -> pow { inf { U } }

Gate specific canonicalizations

  • Rotation gates - pow scales θ:
    pow(r) { G(θ, ...) } → G(r·θ, ...)
    G in {GPhase, RX, RY, RZ, P, RXX, RYY, RZX, RZZ, R(θ,φ), XX+YY(θ,β), XX-YY(θ,β)}
  • Diagonal / P-Gates: Z=P(π), S=P(π/2), Sdg=P(-π/2), T=P(π/4), Tdg=P(-π/4):
    pow(r) { G } → named gate for normalized r*base_angle, else P(r*base_angle)
    All gates are implicitly converted and scaled as a P gate i.e. G^r = P(r*base_angle). Then we check if r*base_angle is again a base angle of a named gate: match r*base_angle with: 0 → erase, ±π → Z, π/2 → S, -π/2 → Sdg, π/4 → T, -π/4 → Tdg
  • Pauli X/Y:
    pow(r) { X/Y } → gphase(-r·π/2); RX/RY(r·π)
    X only: r=½ → SX, r=-½ → SXdg
  • SX / SXdg — symmetric pair:
    |r| = 2 → X
    pow(r) { SX } → gphase(-r·π/4); RX( r·π/2)
    pow(r) { SXdg } → gphase(+r·π/4); RX(-r·π/2)
  • Hermitian gates (only for integer exponent)
    pow(n) { H / ECR / SWAP } → erase for even exponent
    else → H / ECR / SWAP for odd exponent
  • iSWAP
    pow(r) { iSWAP } → XX+YY(-r·π, 0)
  • Pass-through:
    pow(r) { Id / Barrier } → Id / Barrier

Notes

  • There is currently no conversion to the jeff dialect
  • A lot of code between InvOp and PowOp is duplicated (extracting operations, reordering, etc. is basically the same) - maybe its a good idea to unify by creating utility functions, which all modifiers can use - this might be only worth doing, if more modifiers would be implemented (which is unlikely).
  • Instead of converting pow(r) { inv { U }} to pow(-r) { U } we leave it as is and instead convert the other way around (see Negative Power Canonicalization)

Related Issues

Fixes #1129
Fixes #1131

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

@burgholzer burgholzer added feature New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels Apr 1, 2026
@burgholzer burgholzer added this to the MLIR Support milestone Apr 1, 2026
@burgholzer burgholzer linked an issue Apr 1, 2026 that may be closed by this pull request
@J4MMlE J4MMlE force-pushed the pow-modifier branch 2 times, most recently from f017b29 to 4964ed0 Compare April 7, 2026 11:13

// Move supporting ops (constants, arithmetic) out of the body so their
// Values are accessible from outside and survive PowOp erasure.
for (auto& bodyOp : llvm::make_early_inc_range(*op.getBody())) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because inv creates a negf operation inside its body we must move out these operations when we want to apply gate specific canonicalizations. Currently I do this here unconditionally - which feels pretty wrong. Isnt there a way for hoisting up operations which can/should be moved out of the body? Should maybe inv and pow run such a hositing up thing?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure that these operations are automatically hoisted up as part of canonicalization.
I remember this being problematic for nested Control and Inverse modifiers already. But we never had to handle that explicitly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code for inv { pow (2) { rx(0.123) }}:

module {
  func.func @testPow() {
    %q0_0 = qco.alloc : !qco.qubit

    %q0_1 = qco.inv (%a = %q0_0){
      %a_1 = qco.pow (2.000000e+00) (%b = %a) {
        %c_0 = arith.constant 0.123 : f64
        %b_1 = qco.rx(%c_0) %b : !qco.qubit -> !qco.qubit
        qco.yield %b_1
      } : {!qco.qubit} -> {!qco.qubit}
      qco.yield %a_1
    } : {!qco.qubit} -> {!qco.qubit}
    qco.sink %q0_1 : !qco.qubit
    return
  }
}

will lead to this state (swap pow and inv and then do ReplaceWithKnownGates for inv and rx:

module {
  func.func @testPow() {
    %cst = arith.constant 1.230000e-01 : f64
    %0 = qco.alloc : !qco.qubit
    %1 = qco.pow(2.000000e+00) (%arg0 = %0) {
      %2 = arith.negf %cst : f64
      %3 = qco.rx(%2) %arg0 : !qco.qubit -> !qco.qubit
      qco.yield %3
    } : {!qco.qubit} -> {!qco.qubit}
    qco.sink %1 : !qco.qubit
    return
  }
}

If %2 is not explicitly moved out of the pow modifier it is not possible to merge pow and rx with the known canonicalization. So leaving the code out leads to segfault (ask me how I know lol).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the segfault possibly occurs, because we want to create pow(r) { rx(θ) } → rx(r*θ) with r = %2. %2 only exists inside the body of pow which gets replaced/deleted when doing replaceOpWithNewOp, creating a dangling reference to %2.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. This very much depends on the sequence of the individual canonicalization passes.
Because there definitely is a canonicalization that will pull out the %2 = arith.negf %cst : f64 and fold it into the %cst = arith.constant 1.230000e-01 : f64 statement, which would result in the elimination of the arith.negf instruction and would also eliminate the segfault cause.

There has to be a way to address this. Either by making the canonicalization more robust (e.g., by moving the body of the original pow operation to the outside and then performing the manipulation) or by marking the canonicalization only as valid whenever the body of the operation only consists of the UnitaryOperation (and the yield).
The first solution seems to be more desirable to me.
This is kind of like inlining plus some additional manipulation.
I am pretty sure https://mlir.llvm.org/doxygen/classmlir_1_1RewriterBase.html#a4307b18bc25cec55349236d556cc89bd should do the trick here.
The canonicalization is basically inlining the block and adding a arith.mulf (or whatever the instruction is called) on the rotation angle argument before replacing the rotation angle argument with the new rotation angle.

@J4MMlE

J4MMlE commented Apr 11, 2026

Copy link
Copy Markdown
Contributor Author

Ok, I discovered another problem, consider this example:

inv {
  pow(2.0) {
    ctrl(q) {
      pow(1.5) {
        sx
      }
    }
  }
}

pow(1.5) { sx } will fold to gphase(-(3/2)*π/4); rx((3/2)*π/2) - it creates two operations operating on the same qubit.
For now, when this fold evaluates inside a modifier body, its simply not allowed to happen.
However in theory there should be a solution were this is possible right?
A solution is to push the gphase operation out of the modifiers to keep them in a valid state:
The only case we had to handle would be phase specific operations - 2 operations inside a modifier only occurs when a canonicalization introduces an additional gphase operation.
With the first canonicalization we get this:

inv {
  pow(2.0) {
    ctrl(q) {
      gphase(-3*π/8);
      rx(3*π/4)
    }
  }
}

My idea then: the gphase is pushed up level by level. Each modifier has to modify it differently (inv inverts, pow multiplies and ctrl converts to phase):

inv {
  pow(2.0) {
    p(-3*π/8);
    ctrl(q) {
      rx(3*π/4)
    }
  }
}

then

inv {
  p(-3*π/4);
  pow(2.0) {
    ctrl(q) {
      rx(3*π/4)
    }
  }
}

and then

  p(3π/4)
  inv {
    pow(2.0) {
      ctrl(q) {
        rx(3*π/4)
      }
    }
  }

The only thing im not to sure about, whether the gphase can be moved out of the ctrl by converting it to a phase (p) operation.

Concrete implementation: instead of emiting 2 operations, the canonicalization creates an inner operation and a preamble.
The preamble then gets passed to the parent modifier by calling a transformPreamblefunction
Each modifier has to implement transformPreamble:

  1. transformPreamble transforms the preamble in a modifier specific way
  2. it looks if another parent modifier exists and possibly passes the transformed preamble to it.
  3. if no parent modifier exists inserts the transformed preamble right before itself.

I dont know if creating a whole interface for that is premature, i believe only the power modifier emits 2-operation-canonicalizations so the whole thing could stay within power modifier.
Any thoughts on this @DRovara @burgholzer?

edit: maybe, when keeping it general it would make sense having this as an interface, so that other operations could be moved out as well (like arithmetic operations which e.g. inv introduces).

@mergify mergify Bot added the conflict label Apr 11, 2026
@J4MMlE J4MMlE force-pushed the pow-modifier branch 2 times, most recently from 2375c9d to 993a3eb Compare April 11, 2026 22:18
@mergify mergify Bot removed the conflict label Apr 11, 2026
@J4MMlE J4MMlE force-pushed the pow-modifier branch 5 times, most recently from faeba24 to 05c9335 Compare April 12, 2026 08:52
@J4MMlE

J4MMlE commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

While implementing the QC<->QCO conversion tests, I noticed some friction:
Instead of testing only the conversion function, you have to keep the full pipeline in mind - for both reference and input programs, QC- and QCO-cleanup runs before/after the conversion.
For inv and pow modifiers specifically, this forces you to pick test cases where the modifier survives all surrounding rewrites - otherwise cleanup destroys it before/after conversion, and the test becomes unusable for its intended use.
I think the tests should only invoke the conversion and check its output directly, without the surrounding pipeline interfering.

@J4MMlE J4MMlE force-pushed the pow-modifier branch 2 times, most recently from 91700af to b1759c7 Compare April 12, 2026 09:46
@J4MMlE

J4MMlE commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

Ok, commits cleaned up, linting and coverage succeeds - i think this is ready for a first look @DRovara @burgholzer

@J4MMlE J4MMlE marked this pull request as ready for review April 12, 2026 10:16
@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c

📥 Commits

Reviewing files that changed from the base of the PR and between eadac62 and ad89dd9.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qc_programs.cpp

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added pow modifier operations in both QC and QCO dialects, supporting numeric or SSA-valued exponents and region/body-style usage.
    • Added pow builder APIs for creating and composing pow regions.
    • Canonicalization and folding simplify common exponents and rewrite pow into equivalent gate forms.
    • Added QC↔QCO conversion support for pow, preserving semantics across representations.
  • Tests
    • Expanded unit tests to cover pow canonicalization, folding, control/inverse interactions, and conversion equivalence.
  • Chores
    • Minor header/changelog updates.

Walkthrough

Adds a PowOp modifier across QC and QCO: TableGen op declarations, builder APIs, bidirectional conversions, canonicalization/folding and unitary-matrix support, math helpers, and broad unit-test/program-builder coverage for pow semantics.

Changes

PowOp modifier

Layer / File(s) Summary
TableGen, declarations & shared helpers
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h, mlir/include/mlir/Dialect/QC/IR/QCOps.td, mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h, mlir/include/mlir/Dialect/QCO/IR/QCOps.td, mlir/include/mlir/Dialect/Utils/Utils.h, CHANGELOG.md
Add qc.pow and qco.pow operation declarations with unitary modifiers and single-block regions, public builder method declarations for QCProgramBuilder::pow and QCOProgramBuilder::pow, exponent/angle utility functions (isIntegerExponent, isEvenExponent, normalizeAngle), and changelog updates.
QC PowOp implementation & tests
mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp, mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp, mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp, mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp, mlir/unittests/programs/qc_programs.cpp, mlir/unittests/programs/qc_programs.h
Implement QC PowOp builder, verification, canonicalization patterns (inline/erase/merge/fold), gate-specific parameter scaling and decomposition, add InvPowToNegPow rewrite, and extensive QC program builders and IR tests covering single/multi-qubit gates, parameter scaling, and modifier interactions.
QCO PowOp implementation & tests
mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp, mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp, mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp, mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp, mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp, mlir/unittests/programs/qco_programs.cpp, mlir/unittests/programs/qco_programs.h
Implement QCO PowOp builder with qubit block-arg threading, verification, canonicalization patterns, unitary-matrix computation (body extraction for p==1 and p==0, nullopt for general p), add InvPowToNegPow rewrite, and QCO program builders with reference variants and IR/matrix test coverage.
Bidirectional conversions
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp, mlir/lib/Conversion/QCToQCO/QCToQCO.cpp, mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp, mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
Implement ConvertQCOPowOp (clone body, remap block args) and ConvertQCPowOp (update qubit mapping, thread region args), register conversion patterns, and add conversion tests validating QC↔QCO lowering for pow-over-controlled gates.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant QCBuilder as QC ProgramBuilder
  participant QCPowOp as qc::PowOp
  participant Converter as QC↔QCO Converter
  participant Canon as Canonicalizer

  User->>QCBuilder: pow(exponent, body)
  QCBuilder->>QCPowOp: create with exponent and body callback
  QCPowOp->>QCPowOp: verify structure (exponent constant, single unitary op)
  
  alt convert to QCO
    Converter->>Converter: resolve mapped qubits
    Converter->>Converter: create qco.pow, move body region
    Converter->>Converter: add entry block qubit args
  end
  
  QCPowOp->>Canon: enqueue for canonicalization
  Canon->>Canon: apply patterns (inline pow(1), erase pow(0))
  Canon->>Canon: merge nested pow, move ctrl outside
  Canon->>Canon: fold into gate (scale params, decompose)
  
  alt folding succeeds
    Canon->>User: replace with simplified gates/parameters
  else folding inapplicable
    Canon->>User: leave PowOp unchanged
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • munich-quantum-toolkit/core#1330 — Extends inv modifier infrastructure and canonicalizations used alongside PowOp interactions via InvPowToNegPow rewrite.
  • munich-quantum-toolkit/core#1620 — Refactors QC↔QCO conversion pipelines to table-driven pattern registration, overlapping with PowOp conversion-pattern registration.

Suggested reviewers

  • burgholzer
  • denialhaag

Poem

🐰
I hopped through builders, matrices and trees,
Wrapped gates in powers with quantum-y ease,
Exponents twirl and tests take flight,
Folds and conversions snugly right,
A rabbit cheers — pow shines tonight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '✨ Add power modifier to MLIR' directly summarizes the main change: implementing a power modifier for the QC and QCO MLIR dialects.
Description check ✅ Passed The PR description provides comprehensive details on the power modifier implementation, canonicalizations, gate-specific rewrites, addresses linked issues #1129 and #1131, and includes filled checklist items acknowledging AI assistance.
Linked Issues check ✅ Passed The PR successfully implements core requirements from #1129 (unified modifier framework with verifiers/canonicalizations) and #1131 (pow(r) with integer support, canonicalizations, and importer integration).
Out of Scope Changes check ✅ Passed All changes are directly related to adding the power modifier to QC and QCO dialects, including API extensions, canonicalizations, conversions, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
CHANGELOG.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (1)

68-76: ⚠️ Potential issue | 🔴 Critical

The rewrite set is broader than the body invariant it actually preserves.

verify() admits helper ops before the body unitary, but NegPowToInvPow and MoveCtrlOutside clone only the unitary, MergeNestedPow only retargets the moved op’s qubit operands, and the identity-producing branches replace pow with plain pass-through values. With body-local arith.* or captured values, that can dangle defs after replacement; with a nested parent modifier, it can leave the parent region with no unitary at all. Either tighten verify() back to the single-unitary form, or preserve/hoist the full prologue and rewrite enclosing modifiers in the same pattern.

Also applies to: 158-169, 183-199, 218-221, 244-259, 284-289, 482-499, 596-655

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 68 - 76, The
rewrite patterns (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow, and functions
like tryReplaceWithNamedPhaseGate operating on PowOp) assume a single-unitary
body but verify() currently allows helper ops before the unitary; this leads to
dangling defs or removal of the unitary while leaving prologue helpers behind.
Fix by either tightening verify() on PowOp to require exactly one unitary op in
the body (no prologue helpers), or update each rewrite (NegPowToInvPow,
MoveCtrlOutside, MergeNestedPow and tryReplaceWithNamedPhaseGate) to
preserve/hoist the full prologue: when cloning or retargeting the unitary, also
clone or move any preceding helper ops and update their uses, ensure
replacements replace the entire region (including helpers) or remap all
results/operands so no defs are left dangling, and update operand/region
remapping logic to maintain the same external SSA edges for PowOp.
mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)

60-68: ⚠️ Potential issue | 🔴 Critical

These rewrites assume a stricter body invariant than verify() enforces.

verify() currently permits arbitrary non-unitary ops before the body unitary, but the structural rewrites only preserve getBodyUnitary(), and some folds erase PowOp outright. With body-local arith.* defs, that can dangle operands after replacement; with a nested parent modifier, it can leave the parent body with no unitary at all. Either tighten the body back to unitary + yield, or hoist/inline the full prologue and rewrite the enclosing modifier atomically.

Based on learnings, InvOp::verify intentionally restricts the body region to exactly two operations (one unitary + yield).

Also applies to: 149-158, 170-173, 188-190, 204-207, 230-235, 431-450, 498-517

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td`:
- Around line 1039-1047: The target/control accessors for the pow op are
inconsistent: getNumTargets() and getTarget(size_t) use overall qubit semantics
while getTargets() delegates to getBodyUnitary(), causing mismatched behavior;
update the pow op's accessors to mirror the InvOp pattern by delegating
target/control-related calls to getBodyUnitary() instead of using
getNumQubits()/getQubit(), i.e., make getNumTargets() call
getBodyUnitary().getNumTargets(), make getTarget(size_t i) call
getBodyUnitary().getTarget(i), and ensure
getNumControls()/getControls()/getControl() follow the same body-unitary
delegation so all UnitaryInterface accessors are consistent with
getBodyUnitary().

In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 71-75: The current replacement only clones
innerPow.getBodyUnitary().getOperation(), losing any non-terminator preamble ops
(constants/arithmetic) that feed the unitary; update the replacement passed to
rewriter.replaceOpWithNewOp<PowOp> / InvOp::create to iterate over
innerPow.getBody() and clone every non-terminator operation (preserving original
order) into the new region instead of cloning only innerPow.getBodyUnitary(), so
values used by the unitary remain available after MovePowOutside transforms.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 173-201: The NegPowToInvPow rewrite flips sign of the exponent by
replacing pow(p){...} with pow(-p){inv{...}}, which yields conjugated
eigenphases for operators that have eigenvalue -1 and thus produces incorrect
results; modify NegPowToInvPow (the matchAndRewrite for PowOp using
getExponentValue and creating an InvOp) to only apply when the operand unitary's
spectrum is guaranteed not to contain −1 (e.g., obtain the unitary via
getUnitaryMatrix() or a new op method and check no eigenvalue is within an
epsilon of -1), and otherwise skip the rewrite (return failure); alternatively
remove the NegPowToInvPow pattern entirely if no safe spectral test is
available.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp`:
- Around line 91-109: Add tests that exercise fractional powers of RXX to hit
the eigendecomposition/branch logic: create additional test cases in
QCOMatrixTest (e.g., new TEST_F methods or extend PowRxxOpMatrix) that build a
module with powRxx at fractional exponents like 0.5 and -0.5 (use the same
QCOProgramBuilder entry used for powRxx but with the power changed), retrieve
the PowOp via func::FuncOp and PowOp/getUnitaryMatrix(), compute the expected
matrix via dd::opToTwoQubitGateMatrix(qc::OpType::RXX, {theta * power}) into an
Eigen::Matrix4cd, and assert matrix->isApprox(eigenDefinition) for both 0.5 and
-0.5 to validate principal-branch/eigendecomposition behavior.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 893-894: The test case name string in the QCOTestCase entry is
incorrect: change QCOTestCase{"TdgThenS", MQT_NAMED_BUILDER(tdgThenT),
MQT_NAMED_BUILDER(emptyQCO)} so the human-readable name matches the builder by
renaming "TdgThenS" to "TdgThenT" (to align with the tdgThenT builder and
emptyQCO), ensuring test failure messages are not misleading.

In `@mlir/unittests/programs/qc_programs.cpp`:
- Around line 311-315: The global-phase sign is inverted in the reference
decompositions: in powThirdXRef change the gphase argument from -1.0/3.0 *
std::numbers::pi / 2.0 to +1.0/3.0 * std::numbers::pi / 2.0 so the reference
circuit matches the U/R* convention (i.e., X = gphase(+π/2)·rx(π)); apply the
same flip of sign for the gphase calls in the other reference functions
mentioned (the blocks at 357-361, 733-737, 789-793) so they use positive phase
corrections consistent with GPhaseOp(inputPhase - outPhase).

---

Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 68-76: The rewrite patterns (NegPowToInvPow, MoveCtrlOutside,
MergeNestedPow, and functions like tryReplaceWithNamedPhaseGate operating on
PowOp) assume a single-unitary body but verify() currently allows helper ops
before the unitary; this leads to dangling defs or removal of the unitary while
leaving prologue helpers behind. Fix by either tightening verify() on PowOp to
require exactly one unitary op in the body (no prologue helpers), or update each
rewrite (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow and
tryReplaceWithNamedPhaseGate) to preserve/hoist the full prologue: when cloning
or retargeting the unitary, also clone or move any preceding helper ops and
update their uses, ensure replacements replace the entire region (including
helpers) or remap all results/operands so no defs are left dangling, and update
operand/region remapping logic to maintain the same external SSA edges for
PowOp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e7ba28cd-bad0-4888-8e3a-03526f96e57b

📥 Commits

Reviewing files that changed from the base of the PR and between 3a02526 and b1759c7.

📒 Files selected for processing (22)
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h

Comment thread mlir/include/mlir/Dialect/QC/IR/QCOps.td Outdated
Comment thread mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp Outdated
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp Outdated
Comment thread mlir/unittests/programs/qc_programs.cpp
@burgholzer

Copy link
Copy Markdown
Member

While implementing the QC<->QCO conversion tests, I noticed some friction: Instead of testing only the conversion function, you have to keep the full pipeline in mind - for both reference and input programs, QC- and QCO-cleanup runs before/after the conversion. For inv and pow modifiers specifically, this forces you to pick test cases where the modifier survives all surrounding rewrites - otherwise cleanup destroys it before/after conversion, and the test becomes unusable for its intended use. I think the tests should only invoke the conversion and check its output directly, without the surrounding pipeline interfering.

The problem with just invoking the conversion itself is that the verification becomes very hard at that point since canonicalization has not been run. Constants might be in different places. Some canonicalizations on classical constructs may not have been applied, etc.
I fully see the point though that it becomes a bit harder to write meaningful conversion tests for the modifiers, since most of them are resolved on the dialect canonicalization level already.
However, as long as we have no better means for running verification, I don't really see a good way to go forward without the cleanup passes running before the conversion.

@burgholzer

Copy link
Copy Markdown
Member

Ok, I discovered another problem, consider this example:

inv {
  pow(2.0) {
    ctrl(q) {
      pow(1.5) {
        sx
      }
    }
  }
}

Hm. This one is tricky.

pow(1.5) { sx } will fold to gphase(-(3/2)*π/4); rx((3/2)*π/2) - it creates two operations operating on the same qubit.

Technical nitpick, but these operations do not operate on the same qubit since the gphase operation has no target qubits.

For now, when this fold evaluates inside a modifier body, its simply not allowed to happen. However in theory there should be a solution were this is possible right? A solution is to push the gphase operation out of the modifiers to keep them in a valid state: The only case we had to handle would be phase specific operations - 2 operations inside a modifier only occurs when a canonicalization introduces an additional gphase operation. With the first canonicalization we get this:

inv {
  pow(2.0) {
    ctrl(q) {
      gphase(-3*π/8);
      rx(3*π/4)
    }
  }
}

My idea then: the gphase is pushed up level by level. Each modifier has to modify it differently (inv inverts, pow multiplies and ctrl converts to phase):

inv {
  pow(2.0) {
    p(-3*π/8);
    ctrl(q) {
      rx(3*π/4)
    }
  }
}

then

inv {
  p(-3*π/4);
  pow(2.0) {
    ctrl(q) {
      rx(3*π/4)
    }
  }
}

and then

  p(3π/4)
  inv {
    pow(2.0) {
      ctrl(q) {
        rx(3*π/4)
      }
    }
  }

The only thing im not to sure about, whether the gphase can be moved out of the ctrl by converting it to a phase (p) operation.

This is definitely valid as it is also one of the existing canonicalizations for the CtrlOp when that op contains a GphaseOp.

Before commenting on the proposal below, a couple of observations:
Conceptually, we would want this chain of modifiers to resolve in a fixed order.
Essentially, CtrlOp shall be moved as far out as possible, InvOp as far in as possible.
Then InvOp canonicalizations shall happen, followed by PowOp canonicalizations.
This would mean that the example from above first becomes

inv {
  pow(2.0) {
    ctrl(q0) {
      pow(1.5) {
        sx q1;
      }
    }
  }
}

-->

ctrl(q0) {
  pow(2.0) {
    pow(1.5) {
      inv {
        sx q1;
      }
    }
  }
}

-->

ctrl(q0) {
  pow(3.0) {
    inv {
      sx q1;
    }
  }
}

-->

ctrl(q0) {
  pow(3.0) {
    sxdg q1;
  }
}

At this stage, the real problem appears, because the pow canonicalization would now create two operations inside the modifier.
This is somewhat special, because the only operation that would ever be generated as an extra operation at the moment is a global phase operation, which can be pulled out of the containing control operation (in case of one control qubit) or converted to a controlled phase gate (in case of more than one control).

The easy way out here is to not permit such PowOp rewrites in CtrlOp modifiers.
Specifically, rewrites that would produce more than one gate.
And, to be honest, I think I would be fine with that for the PR at hand.
We could then open a tracking issue for extending support to these special cases.
Without having looked too deeply into the code yet, I would assume that there aren't too many of these special cases, right?

Concrete implementation: instead of emiting 2 operations, the canonicalization creates an inner operation and a preamble. The preamble then gets passed to the parent modifier by calling a transformPreamblefunction Each modifier has to implement transformPreamble:

  1. transformPreamble transforms the preamble in a modifier specific way
  2. it looks if another parent modifier exists and possibly passes the transformed preamble to it.
  3. if no parent modifier exists inserts the transformed preamble right before itself.

I dont know if creating a whole interface for that is premature, i believe only the power modifier emits 2-operation-canonicalizations so the whole thing could stay within power modifier. Any thoughts on this @DRovara @burgholzer?

edit: maybe, when keeping it general it would make sense having this as an interface, so that other operations could be moved out as well (like arithmetic operations which e.g. inv introduces).

I believe I like the general direction of this, but I would not want to create that complexity as part of this PR. Let's rather get a working solution here for the majority of the cases and live with the fact that some constructs cannot be resolved at the moment, even though it would be technically possible.
Does that make sense.

@J4MMlE

J4MMlE commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (2)
mlir/include/mlir/Dialect/QC/IR/QCOps.td (1)

1016-1022: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify negative fractional-power semantics in the op description.

Line 1022 still documents r < 0 as inv @ pow(-r) @ g unconditionally, but that rewrite is only sound for integer exponents. For fractional negative powers, the canonical form must stay as pow(-r){U} because principal-branch semantics differ on unitaries with eigenvalue -1 (e.g. H, X, Y, Z, ECR, SWAP).

Based on learnings, NegPowToInvPow is only mathematically correct for integer exponents and negative fractional exponents must not be documented as equivalent to pow(-r){inv{U}}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td` around lines 1016 - 1022, Update
the QCOps.td description for the modifier operation to clarify negative-power
semantics: state that the rewrite "r < 0: inv @ pow(-r) @ g" is only valid for
integer exponents and must not be applied to fractional negative exponents; for
negative fractional r the canonical form remains pow(-r){U} because
principal-branch semantics can differ for unitaries with eigenvalue -1 (e.g. H,
X, Y, Z, ECR, SWAP); also reference the transformation rule name NegPowToInvPow
to indicate it is restricted to integer exponents.
mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)

166-203: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Clone the full body when rebuilding nested modifiers.

NegPowToInvPow, MergeNestedPow, and MoveCtrlOutside only clone getBodyUnitary(). For cases like pow(-2) { rx(0.123) }, the arith.constant for 0.123 lives in the old body, so the cloned gate in the new modifier still references a value from the region being erased. That produces invalid IR and reintroduces the dangling-value failure you already handled in FoldPowIntoGate.

Please inline/move the non-YieldOp body ops together with the unitary, or clone/remap the unitary’s operand-def chain into the new body before replacing op.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 166 - 203,
NegPowToInvPow, MergeNestedPow and MoveCtrlOutside currently only clone
getBodyUnitary() into the newly created modifier body which leaves upstream ops
(e.g. constants) referenced from the old region and causes dangling-value IR;
fix each pattern (the matchAndRewrite implementations that call
rewriter.replaceOpWithNewOp and PowOp::create / InvOp::create) to also clone or
move the non-Yield ops that the unitary depends on into the new region (or
clone/remap the unitary’s operand-def chain) before replacing the original op so
all SSA operands in the new body point to values inside the new region (similar
to the work done in FoldPowIntoGate but applied here to preserve constants and
other defs).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 489-535: PowOp currently assumes the exponent is a constant in
getExponentValue(), but verify() doesn't enforce that; update PowOp::verify() to
reject dynamic exponents by checking getExponent() is an arith.constant (use
matchPattern(getExponent(), m_Constant(&attr)) or equivalent) and emit an op
error (e.g., "exponent must be a constant") when it is not, so
getExponentValue() is only called on verified ops; reference
PowOp::getExponentValue, PowOp::verify, getExponent(), and
matchPattern/m_Constant.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 624-683: PowOp::verify must reject non-constant exponents to avoid
hard aborts in downstream canonicalizations; update PowOp::verify to check that
the exponent operand (getExponent()) is a constant floating-point value (the
same form expected by getExponentValue()/getUnitaryMatrix()) and emitOpError if
it is not, so IR with non-constant exponents cannot be constructed, or
alternatively adjust getExponentValue() callers to gracefully bail out—prefer
the constants-only fix by adding a const-check in PowOp::verify referencing
getExponent() and getExponentValue() to locate the relevant logic.

In `@mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp`:
- Around line 149-157: Add the missing regression tests that cover fractional
exponents and the control-modifier context: add a QCTestCase that exercises
inv(pow(0.5){H}) and expects pow(-0.5){H} (use the appropriate builders, e.g.,
invPowHFrac and powHFracNeg) and add a QCTestCase that instantiates ctrlPowSx
with its reference (e.g., ctrlPowSx and ctrlPowSxRef) so the ctrl { pow(1/3){sx}
} path stays blocked; ensure these new cases reference the same test harness
symbols as the others and rely on MovePowOutside emitting pow(-p){U} (not
wrapping in inv).

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 255-275: Add the missing regression case for the multi-op
expansion path by adding the ctrlPowSx test into the INSTANTIATE_TEST_SUITE_P
list: create a QCOTestCase entry using MQT_NAMED_BUILDER(ctrlPowSx) and the
appropriate reference builder (e.g., MQT_NAMED_BUILDER(ctrlPowSxRef) or the
existing reference name from qco_programs.cpp) so the INSTANTIATE_TEST_SUITE_P
(QCOPowOpTest, QCOTest, ...) covers the ctrlPowSx scenario that triggers
sx->gphase+rx expansion and validates the PowOp-in-modifier bug.
- Around line 267-268: The test case using QCOTestCase{"NegPowH",
MQT_NAMED_BUILDER(negPowH), MQT_NAMED_BUILDER(negPowH)} incorrectly
self-compares; change it to assert structure instead of using negPowH as the
reference so the rewrite NegPowToInvPow cannot hide an invalid transformation.
Update the "NegPowH" test to run the pipeline on negPowH and then verify the
cleaned IR still contains a pow node with a negative fractional exponent applied
to H (e.g., pow(-0.5){ h } or equivalent AST node), rather than comparing
against the same builder; locate the QCOTestCase entry for "NegPowH" and replace
the reference builder with an assertion helper that inspects the resulting IR
for a pow node with exponent -0.5 and base H (or fails if an inv-based form
appears).
- Around line 263-264: The test compares powRxx to itself so a broken
angle-scaling rewrite would still pass; update the QCOTestCase entry for
"PowRxx" to use a distinct reference builder that emits the already-scaled rxx
(i.e., rxx(2θ)) instead of MQT_NAMED_BUILDER(powRxx) twice. Replace the second
MQT_NAMED_BUILDER(powRxx) with a dedicated builder function name (e.g.,
powRxxScaled or powRxxRef) that constructs the expected canonical form, or
supply an inline/reference builder that produces rxx with the scaled angle;
ensure the unique symbol you add is used in the test entry so the case compares
the rewritten result to the correct scaled reference.

In `@mlir/unittests/programs/qco_programs.h`:
- Around line 1184-1212: Add a regression case that constructs inv(pow(0.5){H})
and asserts it canonicalizes to the same form produced by negPowH rather than to
pow(0.5){inv{H}}; specifically, in the test suite create a new helper/test that
uses QCOProgramBuilder to build inv(pow(0.5){H}), run the
canonicalization/passes that trigger InvPowToNegPow, and compare the result
against negPowH (reuse negPowH as the expected reference); ensure the test
references the inv(pow(...){...}) source form and the negPowH reference so the
new rewrite is exercised and validated.

---

Duplicate comments:
In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td`:
- Around line 1016-1022: Update the QCOps.td description for the modifier
operation to clarify negative-power semantics: state that the rewrite "r < 0:
inv @ pow(-r) @ g" is only valid for integer exponents and must not be applied
to fractional negative exponents; for negative fractional r the canonical form
remains pow(-r){U} because principal-branch semantics can differ for unitaries
with eigenvalue -1 (e.g. H, X, Y, Z, ECR, SWAP); also reference the
transformation rule name NegPowToInvPow to indicate it is restricted to integer
exponents.

In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 166-203: NegPowToInvPow, MergeNestedPow and MoveCtrlOutside
currently only clone getBodyUnitary() into the newly created modifier body which
leaves upstream ops (e.g. constants) referenced from the old region and causes
dangling-value IR; fix each pattern (the matchAndRewrite implementations that
call rewriter.replaceOpWithNewOp and PowOp::create / InvOp::create) to also
clone or move the non-Yield ops that the unitary depends on into the new region
(or clone/remap the unitary’s operand-def chain) before replacing the original
op so all SSA operands in the new body point to values inside the new region
(similar to the work done in FoldPowIntoGate but applied here to preserve
constants and other defs).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2ab9a6c3-b681-421d-bb07-b876cdca1610

📥 Commits

Reviewing files that changed from the base of the PR and between e868fe9 and eadac62.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h

Comment thread mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp
Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
Comment thread mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp Outdated
Comment on lines +255 to +275
INSTANTIATE_TEST_SUITE_P(
QCOPowOpTest, QCOTest,
testing::Values(QCOTestCase{"Pow1Inline", MQT_NAMED_BUILDER(pow1Inline),
MQT_NAMED_BUILDER(rx)},
QCOTestCase{"Pow0Erase", MQT_NAMED_BUILDER(pow0Erase),
MQT_NAMED_BUILDER(emptyQCO)},
QCOTestCase{"NestedPow", MQT_NAMED_BUILDER(nestedPow),
MQT_NAMED_BUILDER(powSingleExponent)},
QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx),
MQT_NAMED_BUILDER(powRxx)},
QCOTestCase{"NegPowRx", MQT_NAMED_BUILDER(negPowRx),
MQT_NAMED_BUILDER(powRxNeg)},
QCOTestCase{"NegPowH", MQT_NAMED_BUILDER(negPowH),
MQT_NAMED_BUILDER(negPowH)},
QCOTestCase{"InvPowRx", MQT_NAMED_BUILDER(invPowRx),
MQT_NAMED_BUILDER(powRxNeg)},
QCOTestCase{"PowCtrlRx", MQT_NAMED_BUILDER(powCtrlRx),
MQT_NAMED_BUILDER(ctrlPowRx)},
QCOTestCase{"NegPowInvIswap",
MQT_NAMED_BUILDER(negPowInvIswap),
MQT_NAMED_BUILDER(negPowInvIswapRef)}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

The suite is still missing the ctrlPowSx regression case.

The hard PowOp-in-modifier bug for this PR only shows up when pow would expand into multiple ops, e.g. sx -> gphase + rx. The current PowCtrlRx / CtrlPowRx entries stay single-op, and ctrlPowSx from qco_programs.cpp is not instantiated here, so that path is still untested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp` around lines 255 - 275, Add
the missing regression case for the multi-op expansion path by adding the
ctrlPowSx test into the INSTANTIATE_TEST_SUITE_P list: create a QCOTestCase
entry using MQT_NAMED_BUILDER(ctrlPowSx) and the appropriate reference builder
(e.g., MQT_NAMED_BUILDER(ctrlPowSxRef) or the existing reference name from
qco_programs.cpp) so the INSTANTIATE_TEST_SUITE_P (QCOPowOpTest, QCOTest, ...)
covers the ctrlPowSx scenario that triggers sx->gphase+rx expansion and
validates the PowOp-in-modifier bug.

Comment on lines +263 to +264
QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx),
MQT_NAMED_BUILDER(powRxx)},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PowRxx currently can't fail for a broken angle-scaling rewrite.

This compares powRxx against itself, so the case still passes if pow(2){rxx(θ)} forgets to canonicalize to rxx(2θ)}. Please use a distinct reference builder that emits the scaled rxx directly.

Suggested wiring
-                    QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx),
-                                MQT_NAMED_BUILDER(powRxx)},
+                    QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx),
+                                MQT_NAMED_BUILDER(powRxxRef)},
+void powRxxRef(QCOProgramBuilder& b) {
+  auto q = b.allocQubitRegister(2);
+  b.rxx(2.0 * 0.123, q[0], q[1]);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp` around lines 263 - 264, The
test compares powRxx to itself so a broken angle-scaling rewrite would still
pass; update the QCOTestCase entry for "PowRxx" to use a distinct reference
builder that emits the already-scaled rxx (i.e., rxx(2θ)) instead of
MQT_NAMED_BUILDER(powRxx) twice. Replace the second MQT_NAMED_BUILDER(powRxx)
with a dedicated builder function name (e.g., powRxxScaled or powRxxRef) that
constructs the expected canonical form, or supply an inline/reference builder
that produces rxx with the scaled angle; ensure the unique symbol you add is
used in the test entry so the case compares the rewritten result to the correct
scaled reference.

Comment on lines +267 to +268
QCOTestCase{"NegPowH", MQT_NAMED_BUILDER(negPowH),
MQT_NAMED_BUILDER(negPowH)},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

NegPowH needs a structural assertion, not a self-comparison.

This is the exact fractional-negative case that must not go through NegPowToInvPow. Using negPowH as both program and reference runs the same cleanup twice, so an invalid rewrite would still pass. Please make this a dedicated test that asserts the cleaned IR still contains pow(-0.5){ h } rather than an inv-based form.

Based on learnings, NegPowToInvPow is only mathematically sound for integer exponents; negative fractional powers of Hermitian gates like H must remain as pow(-r){U}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp` around lines 267 - 268, The
test case using QCOTestCase{"NegPowH", MQT_NAMED_BUILDER(negPowH),
MQT_NAMED_BUILDER(negPowH)} incorrectly self-compares; change it to assert
structure instead of using negPowH as the reference so the rewrite
NegPowToInvPow cannot hide an invalid transformation. Update the "NegPowH" test
to run the pipeline on negPowH and then verify the cleaned IR still contains a
pow node with a negative fractional exponent applied to H (e.g., pow(-0.5){ h }
or equivalent AST node), rather than comparing against the same builder; locate
the QCOTestCase entry for "NegPowH" and replace the reference builder with an
assertion helper that inspects the resulting IR for a pow node with exponent
-0.5 and base H (or fails if an inv-based form appears).

Comment on lines +1184 to +1212
/// Creates a circuit with pow(-0.5) wrapping H (negative non-integer exponent).
/// Expected to remain unchanged: fractional exponent on a unitary with
/// eigenvalue -1 cannot safely apply NegPowToInvPow.
void negPowH(QCOProgramBuilder& b);

/// Creates a circuit with inv wrapping pow (should reorder to pow wrapping
/// inv).
void invPowRx(QCOProgramBuilder& b);

/// Creates a circuit with pow wrapping ctrl wrapping RX (should move ctrl
/// outside).
void powCtrlRx(QCOProgramBuilder& b);

/// Creates a circuit with ctrl wrapping pow wrapping RX (reference for
/// powCtrlRx).
void ctrlPowRx(QCOProgramBuilder& b);

/// Creates a circuit with pow(-2) wrapping inv wrapping iSWAP.
/// Exercises NegPowToInvPow: inv{iswap} survives InvOp canonicalization,
/// FoldPowIntoGate fails (inner is InvOp), so NegPowToInvPow fires.
void negPowInvIswap(QCOProgramBuilder& b);

/// Reference for negPowInvIswap: xx_plus_yy(-2π, 0) (the fully folded form).
void negPowInvIswapRef(QCOProgramBuilder& b);

/// Creates a circuit with ctrl wrapping pow(1/3) wrapping SX. The fold
/// pow(p){SX} → gphase+rx is suppressed inside ctrl (would emit two ops),
/// so the pow survives canonicalization and reaches ConvertQCOPowOp.
void ctrlPowSx(QCOProgramBuilder& b);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a regression case for inv(pow(0.5){H}).

This header already adds negPowH, but the new InvPowToNegPow rewrite is only really pinned if you also exercise the source form that triggers it: inv(pow(0.5){H}) should canonicalize to that same negative-fractional pow form, not to pow(0.5){inv{H}}. Reusing negPowH as the reference would make this a very small but high-value coverage addition.

Based on learnings: inv(pow(p){U}) must be rewritten by negating the exponent, and negative fractional powers on Hermitian/self-inverse gates must remain as pow(-r){U} because the inverse-inside-power form is not mathematically sound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/unittests/programs/qco_programs.h` around lines 1184 - 1212, Add a
regression case that constructs inv(pow(0.5){H}) and asserts it canonicalizes to
the same form produced by negPowH rather than to pow(0.5){inv{H}}; specifically,
in the test suite create a new helper/test that uses QCOProgramBuilder to build
inv(pow(0.5){H}), run the canonicalization/passes that trigger InvPowToNegPow,
and compare the result against negPowH (reuse negPowH as the expected
reference); ensure the test references the inv(pow(...){...}) source form and
the negPowH reference so the new rewrite is exercised and validated.

@denialhaag

Copy link
Copy Markdown
Member

Now that #1751 has been merged, this will require a significant rebase. I'm more than happy to assist here because many details surrounding modifiers have changed with #1751.

@J4MMlE J4MMlE marked this pull request as draft June 12, 2026 09:43
denialhaag added a commit that referenced this pull request Jun 18, 2026
## Description

This is a rebased version of #1671 after #1671 was automatically closed
when #1751 was merged.

Original description by @J4MMlE:

> ## Description
>
> This PR adds a direct OpenQASM-to-QC translation that bypasses the
`qc::QuantumComputation`.
> 
> The new flow reuses the existing parser and walks the AST directly to
emit QC dialect ops. Skipping `QuantumComputation` removes an
intermediate step and enables previously unsupported QASM3 features,
such as gate modifiers, classical computations, and control flow
structures.
> 
> ### What works
> 
> - All standard gates plus Qiskit-style MCX variants
> - Gate modifiers: `ctrl @`, `negctrl @`, `inv @`, nested combinations
> - Register declarations, qubit/bit allocation
> - Measure, reset, barrier
> - `if`/`else` over quantum statements
> - Hardware qubits convert to `qc.static`
> - Broadcasting (register-width gate calls)
> 
> ### Current limitations
> 
> - Gate parameters must be compile-time constants
> - `pow` modifier is unsupported (but will/should be once
[#1603](#1603) is
merged)
> - Layout pragmas are unsupported (they have no equivalent in `QC`)

## Checklist

- [x] The pull request only contains commits that are focused and
relevant to this change.
- [x] I have added appropriate tests that cover the new/changed
functionality.
- [x] ~~I have updated the documentation to reflect these changes.~~
- [x] I have added entries to the changelog for any noteworthy
additions, changes, fixes, or removals.
- [x] ~~I have added migration instructions to the upgrade guide (if
needed).~~
- [x] The changes follow the project's style guidelines and introduce no
new warnings.
- [x] The changes are fully tested and pass the CI checks.
- [x] I have reviewed my own code changes.

---------

Signed-off-by: Daniel Haag <121057143+denialhaag@users.noreply.github.com>
Co-authored-by: Anatol Bussejahn <anatol.bussejahn@tum.de>
Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
@mergify mergify Bot removed the conflict label Jun 23, 2026
@J4MMlE

J4MMlE commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai continue

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@J4MMlE Sure! Continuing with the review of this PR.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (2)

694-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the matrix-availability predicate match getUnitaryMatrix().

Line 695 returns true for pow(2.0) { x } because x has a known matrix, but lines 723-724 still return std::nullopt for every non-0/1 exponent. Gate hasCompileTimeKnownUnitaryMatrix() on the exponent cases actually implemented, or implement the general power computation.

🛠️ Minimal predicate fix
 bool PowOp::hasCompileTimeKnownUnitaryMatrix() {
-  return all_of(getBody()->getOps<UnitaryOpInterface>(),
-                [](UnitaryOpInterface op) {
-                  return op.hasCompileTimeKnownUnitaryMatrix();
-                });
+  const double p = getExponentValue();
+  if (std::abs(p) >= TOLERANCE && std::abs(p - 1.0) >= TOLERANCE) {
+    return false;
+  }
+  auto bodyUnitary = utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody());
+  return bodyUnitary && bodyUnitary.hasCompileTimeKnownUnitaryMatrix();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 694 - 724, The
hasCompileTimeKnownUnitaryMatrix() method does not check the exponent value,
causing it to return true even when getUnitaryMatrix() returns nullopt for
unsupported exponents. Fix this inconsistency by adding exponent validation to
hasCompileTimeKnownUnitaryMatrix(): use getExponentValue() to retrieve the
exponent and check that it matches one of the supported cases (approximately
equal to 0 or 1 using TOLERANCE comparisons), ensuring the predicate only
returns true when getUnitaryMatrix() can actually produce a result.

621-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-unitary quantum side effects in PowOp::verify.

Line 657 only rejects extra unitary ops, so a pow body can still contain alloc/measure/reset/qtensor mutation before the sole unitary and be treated as a valid power modifier. Mirror the InvOp guard here, and reject duplicate input qubits while tightening the verifier.

🛡️ Suggested verifier tightening
 LogicalResult PowOp::verify() {
   FloatAttr attr;
   if (!matchPattern(getExponent(), m_Constant(&attr))) {
     return emitOpError("exponent must be a constant");
   }
 
   auto& block = *getBody();
+  if (llvm::any_of(block, [](Operation& nested) {
+        return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp,
+                   qtensor::InsertOp>(nested);
+      })) {
+    return emitOpError("body must not contain non-unitary quantum operations "
+                       "or modify a quantum register");
+  }
+
   if (block.getOperations().size() < 2) {
     return emitOpError("body region must have at least two operations");
   }
   const auto numTargets = getNumTargets();
+  SmallPtrSet<Value, 4> uniqueQubitsIn;
+  for (Value target : getQubitsIn()) {
+    if (!uniqueQubitsIn.insert(target).second) {
+      return emitOpError("duplicate qubit found");
+    }
+  }
+
   if (block.getArguments().size() != numTargets) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 621 - 686, The
PowOp::verify() method currently only rejects duplicate unitary operations but
allows non-unitary quantum side effects like alloc, measure, reset, and qtensor
mutations to exist in the body region before the unitary. Strengthen the
verification loop that starts with the ++iter iterator (which currently checks
for extra UnitaryOpInterface operations) to also reject any non-unitary quantum
side effects in the body, mirroring the approach used in InvOp. Additionally,
add validation to reject duplicate input qubits by ensuring each block argument
used by the body unitary is unique and matches the corresponding target argument
without duplication.
mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp (1)

101-112: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

InvPowToNegPow can drop required preamble defs and create dangling uses.

Line 108 inlines only innerPow’s body. If that body uses values defined earlier in invOp’s body, those producer ops are erased with invOp, leaving invalid IR.

🔧 Proposed fix
   const double exponent = innerPow.getExponentValue();
   // Remap InvOp's block arguments to the outer qubits they alias,
   // since the inlined ops may reference them and InvOp is about to be erased.
   for (auto [blockArg, outerQubit] :
        llvm::zip_equal(invOp.getBody()->getArguments(), invOp.getQubits())) {
     rewriter.replaceAllUsesWith(blockArg, outerQubit);
   }
+
+  // Preserve non-unitary preamble ops that may define values captured by the
+  // nested pow body.
+  for (auto& bodyOp : llvm::make_early_inc_range(*invOp.getBody())) {
+    if (&bodyOp != innerPow.getOperation() && !isa<YieldOp>(bodyOp)) {
+      rewriter.moveOpBefore(&bodyOp, invOp);
+    }
+  }

   rewriter.replaceOpWithNewOp<PowOp>(invOp, -exponent, [&] {
     auto* powBody = rewriter.getInsertionBlock();
     rewriter.inlineBlockBefore(innerPow.getBody(), powBody, powBody->begin());
     rewriter.eraseOp(&powBody->back()); // erase the inlined YieldOp
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp` around lines 101 - 112, The issue
is that when inlining only innerPow.getBody() into the new PowOp, any preamble
operations that define values used by innerPow's body will be lost when invOp is
erased. Fix this by identifying and inlining any preamble definitions from
invOp's body (operations that come before the innerPow operation and produce
values used by innerPow) into the new PowOp before inlining innerPow.getBody().
This ensures that dependent definitions are preserved and not dangling
references are created.
mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)

555-580: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tighten PowOp::verify to reject non-unitary quantum ops in the body.

Line 557 validates constant exponent, but the verifier still allows side-effecting non-unitary quantum ops in the preamble. That violates modifier legality and can make canonicalizations unsafe when they restructure body contents.

🔧 Proposed fix
 LogicalResult PowOp::verify() {
   FloatAttr attr;
   if (!matchPattern(getExponent(), m_Constant(&attr))) {
     return emitOpError("exponent must be a constant");
   }

   auto& block = *getBody();
+  if (llvm::any_of(block, [](Operation& op) {
+        return isa<AllocOp, DeallocOp, MeasureOp, ResetOp>(op);
+      })) {
+    return emitOpError("body must not contain non-unitary quantum operations");
+  }
+
   if (block.getOperations().size() < 2) {
     return emitOpError("body region must have at least two operations");
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 555 - 580, The
verification logic in PowOp::verify currently only rejects unitary operations in
the preamble (the loop starting with `for (auto it = ++iter; it != block.rend();
++it)`), but it does not reject non-unitary quantum operations that would
violate modifier legality. Enhance the loop that iterates through preamble
operations to add an additional check that rejects non-unitary quantum ops
alongside the existing rejection of unitary ops, ensuring only operations that
are neither unitary quantum ops nor side-effecting quantum ops are permitted in
the body before the required unitary operation.
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)

725-748: 🩺 Stability & Availability | 🔴 Critical

Use moveRegion with type converter for block argument conversion in qco.pow pattern.

cloneRegionBefore preserves the qco.pow entry block argument types (!qco.qubit), but line 742 replaces them with adaptor.getQubitsIn() which contains !qc.qubit values (post-type-conversion from the OpAdaptor). This creates a type mismatch. The ConvertQCOCtrlOp and ConvertQCOInvOp patterns already use moveRegion with getTypeConverter() to properly convert block argument types before RAUW. Apply the same approach here to ensure type safety.

Proposed fix
-    // Clone body region from QCO to QC
-    auto& dstRegion = qcOp.getRegion();
-    rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end());
-
-    auto& entryBlock = dstRegion.front();
-    const auto numArgs = entryBlock.getNumArguments();
+    auto& srcEntryBlock = op.getRegion().front();
+    const auto numArgs = srcEntryBlock.getNumArguments();
     if (adaptor.getQubitsIn().size() != numArgs) {
       return op.emitOpError() << "qco.pow: entry block args (" << numArgs
                               << ") must match number of target operands ("
                               << adaptor.getQubitsIn().size() << ")";
     }
+
+    // Move body region from QCO to QC and convert block args before RAUW.
+    auto& dstRegion = qcOp.getRegion();
+    if (failed(moveRegion(op.getRegion(), dstRegion, rewriter,
+                          getTypeConverter()))) {
+      return failure();
+    }
+    auto& entryBlock = dstRegion.front();
 
     // Remove all block arguments in the cloned region
     rewriter.modifyOpInPlace(qcOp, [&] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 725 - 748, The
`cloneRegionBefore` call in the qco.pow pattern conversion preserves the
original `!qco.qubit` block argument types, but then replaces them with
`!qc.qubit` values from the adaptor, causing a type mismatch. Instead of using
`cloneRegionBefore` followed by manual argument replacement, use `moveRegion`
with `getTypeConverter()` to properly convert the block argument types during
the region move, similar to how `ConvertQCOCtrlOp` and `ConvertQCOInvOp`
patterns handle this conversion. This will ensure the block arguments are
converted from `!qco.qubit` to `!qc.qubit` types automatically and safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 552-553: The example documentation for the `qc.ctrl` operation in
the QCProgramBuilder.h file is missing the `targets` keyword in the syntax.
Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include
the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align
with the current API documentation and prevent confusion for users copying the
example.

In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 251-257: The function getBodyUnitary calls std::next to advance an
iterator by i positions before checking bounds, which results in undefined
behavior since std::next performs no bounds validation. To fix this, manually
iterate through the filtered unitaries range while counting elements, checking
if the count reaches i and if iteration completes without reaching the target
index. Return the unitary operation when the count matches i, or call
llvm::reportFatalUsageError if iteration completes without finding the i-th
unitary.

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-138: The inlineRegion function currently inlines regions and
then attempts to replace block arguments with type-converted adaptor values,
violating MLIR's same-type RAUW invariant when the adaptor values have different
types than the original block arguments. Modify inlineRegion to use the type
converter to convert the block argument types before performing the
replaceAllUsesWith operation, following the pattern used by moveRegion from
ConversionUtils.h. Change the function signature to return LogicalResult instead
of void to properly communicate conversion failures. Update all call sites of
inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines
868-870, and IfOp pattern around lines 913-918) to handle the returned
LogicalResult.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 403-430: The InvOp::verify() method dereferences
block.getTerminator() without first validating that the body is non-empty and
that the terminator is specifically a YieldOp. Before line 426 where
getTerminator() is called, add a verification check to ensure the terminator
exists and is a YieldOp instance, returning an appropriate emitOpError if either
condition fails. This will prevent null dereferences and reject malformed IR
that contains non-yield terminators.
- Around line 343-355: The EraseEmptyInv struct's matchAndRewrite method removes
InvOp operations without checking if they are nested as the sole body unitary
within another modifier, which violates modifier body constraints. Before the
current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to
detect if the InvOp is nested inside a parent modifier body and handle it
accordingly: for single-target nested cases, replace with an IdOp instead of
removing it; for multi-target nested cases, return failure to prevent the
rewrite. This approach should mirror the handling used in the pow(0) pattern to
maintain parent modifier validity.
- Around line 448-517: The hasCompileTimeKnownUnitaryMatrix() function returns
true for cases where getUnitaryMatrix() returns std::nullopt, causing
inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to
include the same conditional checks that getUnitaryMatrix() implements: it
should verify that getNumBodyUnitaries() returns zero, or that there is a sole
body unitary available, or that getNumTargets() equals one (for the composition
case). This ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.

---

Outside diff comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 725-748: The `cloneRegionBefore` call in the qco.pow pattern
conversion preserves the original `!qco.qubit` block argument types, but then
replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch.
Instead of using `cloneRegionBefore` followed by manual argument replacement,
use `moveRegion` with `getTypeConverter()` to properly convert the block
argument types during the region move, similar to how `ConvertQCOCtrlOp` and
`ConvertQCOInvOp` patterns handle this conversion. This will ensure the block
arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and
safely.

In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 101-112: The issue is that when inlining only innerPow.getBody()
into the new PowOp, any preamble operations that define values used by
innerPow's body will be lost when invOp is erased. Fix this by identifying and
inlining any preamble definitions from invOp's body (operations that come before
the innerPow operation and produce values used by innerPow) into the new PowOp
before inlining innerPow.getBody(). This ensures that dependent definitions are
preserved and not dangling references are created.

In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 555-580: The verification logic in PowOp::verify currently only
rejects unitary operations in the preamble (the loop starting with `for (auto it
= ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary
quantum operations that would violate modifier legality. Enhance the loop that
iterates through preamble operations to add an additional check that rejects
non-unitary quantum ops alongside the existing rejection of unitary ops,
ensuring only operations that are neither unitary quantum ops nor side-effecting
quantum ops are permitted in the body before the required unitary operation.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 694-724: The hasCompileTimeKnownUnitaryMatrix() method does not
check the exponent value, causing it to return true even when getUnitaryMatrix()
returns nullopt for unsupported exponents. Fix this inconsistency by adding
exponent validation to hasCompileTimeKnownUnitaryMatrix(): use
getExponentValue() to retrieve the exponent and check that it matches one of the
supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons),
ensuring the predicate only returns true when getUnitaryMatrix() can actually
produce a result.
- Around line 621-686: The PowOp::verify() method currently only rejects
duplicate unitary operations but allows non-unitary quantum side effects like
alloc, measure, reset, and qtensor mutations to exist in the body region before
the unitary. Strengthen the verification loop that starts with the ++iter
iterator (which currently checks for extra UnitaryOpInterface operations) to
also reject any non-unitary quantum side effects in the body, mirroring the
approach used in InvOp. Additionally, add validation to reject duplicate input
qubits by ensuring each block argument used by the body unitary is unique and
matches the corresponding target argument without duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c

📥 Commits

Reviewing files that changed from the base of the PR and between eadac62 and ad89dd9.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qc_programs.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (2)

694-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the matrix-availability predicate match getUnitaryMatrix().

Line 695 returns true for pow(2.0) { x } because x has a known matrix, but lines 723-724 still return std::nullopt for every non-0/1 exponent. Gate hasCompileTimeKnownUnitaryMatrix() on the exponent cases actually implemented, or implement the general power computation.

🛠️ Minimal predicate fix
 bool PowOp::hasCompileTimeKnownUnitaryMatrix() {
-  return all_of(getBody()->getOps<UnitaryOpInterface>(),
-                [](UnitaryOpInterface op) {
-                  return op.hasCompileTimeKnownUnitaryMatrix();
-                });
+  const double p = getExponentValue();
+  if (std::abs(p) >= TOLERANCE && std::abs(p - 1.0) >= TOLERANCE) {
+    return false;
+  }
+  auto bodyUnitary = utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody());
+  return bodyUnitary && bodyUnitary.hasCompileTimeKnownUnitaryMatrix();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 694 - 724, The
hasCompileTimeKnownUnitaryMatrix() method does not check the exponent value,
causing it to return true even when getUnitaryMatrix() returns nullopt for
unsupported exponents. Fix this inconsistency by adding exponent validation to
hasCompileTimeKnownUnitaryMatrix(): use getExponentValue() to retrieve the
exponent and check that it matches one of the supported cases (approximately
equal to 0 or 1 using TOLERANCE comparisons), ensuring the predicate only
returns true when getUnitaryMatrix() can actually produce a result.

621-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-unitary quantum side effects in PowOp::verify.

Line 657 only rejects extra unitary ops, so a pow body can still contain alloc/measure/reset/qtensor mutation before the sole unitary and be treated as a valid power modifier. Mirror the InvOp guard here, and reject duplicate input qubits while tightening the verifier.

🛡️ Suggested verifier tightening
 LogicalResult PowOp::verify() {
   FloatAttr attr;
   if (!matchPattern(getExponent(), m_Constant(&attr))) {
     return emitOpError("exponent must be a constant");
   }
 
   auto& block = *getBody();
+  if (llvm::any_of(block, [](Operation& nested) {
+        return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp,
+                   qtensor::InsertOp>(nested);
+      })) {
+    return emitOpError("body must not contain non-unitary quantum operations "
+                       "or modify a quantum register");
+  }
+
   if (block.getOperations().size() < 2) {
     return emitOpError("body region must have at least two operations");
   }
   const auto numTargets = getNumTargets();
+  SmallPtrSet<Value, 4> uniqueQubitsIn;
+  for (Value target : getQubitsIn()) {
+    if (!uniqueQubitsIn.insert(target).second) {
+      return emitOpError("duplicate qubit found");
+    }
+  }
+
   if (block.getArguments().size() != numTargets) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 621 - 686, The
PowOp::verify() method currently only rejects duplicate unitary operations but
allows non-unitary quantum side effects like alloc, measure, reset, and qtensor
mutations to exist in the body region before the unitary. Strengthen the
verification loop that starts with the ++iter iterator (which currently checks
for extra UnitaryOpInterface operations) to also reject any non-unitary quantum
side effects in the body, mirroring the approach used in InvOp. Additionally,
add validation to reject duplicate input qubits by ensuring each block argument
used by the body unitary is unique and matches the corresponding target argument
without duplication.
mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp (1)

101-112: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

InvPowToNegPow can drop required preamble defs and create dangling uses.

Line 108 inlines only innerPow’s body. If that body uses values defined earlier in invOp’s body, those producer ops are erased with invOp, leaving invalid IR.

🔧 Proposed fix
   const double exponent = innerPow.getExponentValue();
   // Remap InvOp's block arguments to the outer qubits they alias,
   // since the inlined ops may reference them and InvOp is about to be erased.
   for (auto [blockArg, outerQubit] :
        llvm::zip_equal(invOp.getBody()->getArguments(), invOp.getQubits())) {
     rewriter.replaceAllUsesWith(blockArg, outerQubit);
   }
+
+  // Preserve non-unitary preamble ops that may define values captured by the
+  // nested pow body.
+  for (auto& bodyOp : llvm::make_early_inc_range(*invOp.getBody())) {
+    if (&bodyOp != innerPow.getOperation() && !isa<YieldOp>(bodyOp)) {
+      rewriter.moveOpBefore(&bodyOp, invOp);
+    }
+  }

   rewriter.replaceOpWithNewOp<PowOp>(invOp, -exponent, [&] {
     auto* powBody = rewriter.getInsertionBlock();
     rewriter.inlineBlockBefore(innerPow.getBody(), powBody, powBody->begin());
     rewriter.eraseOp(&powBody->back()); // erase the inlined YieldOp
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp` around lines 101 - 112, The issue
is that when inlining only innerPow.getBody() into the new PowOp, any preamble
operations that define values used by innerPow's body will be lost when invOp is
erased. Fix this by identifying and inlining any preamble definitions from
invOp's body (operations that come before the innerPow operation and produce
values used by innerPow) into the new PowOp before inlining innerPow.getBody().
This ensures that dependent definitions are preserved and not dangling
references are created.
mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)

555-580: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tighten PowOp::verify to reject non-unitary quantum ops in the body.

Line 557 validates constant exponent, but the verifier still allows side-effecting non-unitary quantum ops in the preamble. That violates modifier legality and can make canonicalizations unsafe when they restructure body contents.

🔧 Proposed fix
 LogicalResult PowOp::verify() {
   FloatAttr attr;
   if (!matchPattern(getExponent(), m_Constant(&attr))) {
     return emitOpError("exponent must be a constant");
   }

   auto& block = *getBody();
+  if (llvm::any_of(block, [](Operation& op) {
+        return isa<AllocOp, DeallocOp, MeasureOp, ResetOp>(op);
+      })) {
+    return emitOpError("body must not contain non-unitary quantum operations");
+  }
+
   if (block.getOperations().size() < 2) {
     return emitOpError("body region must have at least two operations");
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 555 - 580, The
verification logic in PowOp::verify currently only rejects unitary operations in
the preamble (the loop starting with `for (auto it = ++iter; it != block.rend();
++it)`), but it does not reject non-unitary quantum operations that would
violate modifier legality. Enhance the loop that iterates through preamble
operations to add an additional check that rejects non-unitary quantum ops
alongside the existing rejection of unitary ops, ensuring only operations that
are neither unitary quantum ops nor side-effecting quantum ops are permitted in
the body before the required unitary operation.
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)

725-748: 🩺 Stability & Availability | 🔴 Critical

Use moveRegion with type converter for block argument conversion in qco.pow pattern.

cloneRegionBefore preserves the qco.pow entry block argument types (!qco.qubit), but line 742 replaces them with adaptor.getQubitsIn() which contains !qc.qubit values (post-type-conversion from the OpAdaptor). This creates a type mismatch. The ConvertQCOCtrlOp and ConvertQCOInvOp patterns already use moveRegion with getTypeConverter() to properly convert block argument types before RAUW. Apply the same approach here to ensure type safety.

Proposed fix
-    // Clone body region from QCO to QC
-    auto& dstRegion = qcOp.getRegion();
-    rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end());
-
-    auto& entryBlock = dstRegion.front();
-    const auto numArgs = entryBlock.getNumArguments();
+    auto& srcEntryBlock = op.getRegion().front();
+    const auto numArgs = srcEntryBlock.getNumArguments();
     if (adaptor.getQubitsIn().size() != numArgs) {
       return op.emitOpError() << "qco.pow: entry block args (" << numArgs
                               << ") must match number of target operands ("
                               << adaptor.getQubitsIn().size() << ")";
     }
+
+    // Move body region from QCO to QC and convert block args before RAUW.
+    auto& dstRegion = qcOp.getRegion();
+    if (failed(moveRegion(op.getRegion(), dstRegion, rewriter,
+                          getTypeConverter()))) {
+      return failure();
+    }
+    auto& entryBlock = dstRegion.front();
 
     // Remove all block arguments in the cloned region
     rewriter.modifyOpInPlace(qcOp, [&] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 725 - 748, The
`cloneRegionBefore` call in the qco.pow pattern conversion preserves the
original `!qco.qubit` block argument types, but then replaces them with
`!qc.qubit` values from the adaptor, causing a type mismatch. Instead of using
`cloneRegionBefore` followed by manual argument replacement, use `moveRegion`
with `getTypeConverter()` to properly convert the block argument types during
the region move, similar to how `ConvertQCOCtrlOp` and `ConvertQCOInvOp`
patterns handle this conversion. This will ensure the block arguments are
converted from `!qco.qubit` to `!qc.qubit` types automatically and safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 552-553: The example documentation for the `qc.ctrl` operation in
the QCProgramBuilder.h file is missing the `targets` keyword in the syntax.
Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include
the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align
with the current API documentation and prevent confusion for users copying the
example.

In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 251-257: The function getBodyUnitary calls std::next to advance an
iterator by i positions before checking bounds, which results in undefined
behavior since std::next performs no bounds validation. To fix this, manually
iterate through the filtered unitaries range while counting elements, checking
if the count reaches i and if iteration completes without reaching the target
index. Return the unitary operation when the count matches i, or call
llvm::reportFatalUsageError if iteration completes without finding the i-th
unitary.

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-138: The inlineRegion function currently inlines regions and
then attempts to replace block arguments with type-converted adaptor values,
violating MLIR's same-type RAUW invariant when the adaptor values have different
types than the original block arguments. Modify inlineRegion to use the type
converter to convert the block argument types before performing the
replaceAllUsesWith operation, following the pattern used by moveRegion from
ConversionUtils.h. Change the function signature to return LogicalResult instead
of void to properly communicate conversion failures. Update all call sites of
inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines
868-870, and IfOp pattern around lines 913-918) to handle the returned
LogicalResult.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 403-430: The InvOp::verify() method dereferences
block.getTerminator() without first validating that the body is non-empty and
that the terminator is specifically a YieldOp. Before line 426 where
getTerminator() is called, add a verification check to ensure the terminator
exists and is a YieldOp instance, returning an appropriate emitOpError if either
condition fails. This will prevent null dereferences and reject malformed IR
that contains non-yield terminators.
- Around line 343-355: The EraseEmptyInv struct's matchAndRewrite method removes
InvOp operations without checking if they are nested as the sole body unitary
within another modifier, which violates modifier body constraints. Before the
current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to
detect if the InvOp is nested inside a parent modifier body and handle it
accordingly: for single-target nested cases, replace with an IdOp instead of
removing it; for multi-target nested cases, return failure to prevent the
rewrite. This approach should mirror the handling used in the pow(0) pattern to
maintain parent modifier validity.
- Around line 448-517: The hasCompileTimeKnownUnitaryMatrix() function returns
true for cases where getUnitaryMatrix() returns std::nullopt, causing
inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to
include the same conditional checks that getUnitaryMatrix() implements: it
should verify that getNumBodyUnitaries() returns zero, or that there is a sole
body unitary available, or that getNumTargets() equals one (for the composition
case). This ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.

---

Outside diff comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 725-748: The `cloneRegionBefore` call in the qco.pow pattern
conversion preserves the original `!qco.qubit` block argument types, but then
replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch.
Instead of using `cloneRegionBefore` followed by manual argument replacement,
use `moveRegion` with `getTypeConverter()` to properly convert the block
argument types during the region move, similar to how `ConvertQCOCtrlOp` and
`ConvertQCOInvOp` patterns handle this conversion. This will ensure the block
arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and
safely.

In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 101-112: The issue is that when inlining only innerPow.getBody()
into the new PowOp, any preamble operations that define values used by
innerPow's body will be lost when invOp is erased. Fix this by identifying and
inlining any preamble definitions from invOp's body (operations that come before
the innerPow operation and produce values used by innerPow) into the new PowOp
before inlining innerPow.getBody(). This ensures that dependent definitions are
preserved and not dangling references are created.

In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 555-580: The verification logic in PowOp::verify currently only
rejects unitary operations in the preamble (the loop starting with `for (auto it
= ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary
quantum operations that would violate modifier legality. Enhance the loop that
iterates through preamble operations to add an additional check that rejects
non-unitary quantum ops alongside the existing rejection of unitary ops,
ensuring only operations that are neither unitary quantum ops nor side-effecting
quantum ops are permitted in the body before the required unitary operation.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 694-724: The hasCompileTimeKnownUnitaryMatrix() method does not
check the exponent value, causing it to return true even when getUnitaryMatrix()
returns nullopt for unsupported exponents. Fix this inconsistency by adding
exponent validation to hasCompileTimeKnownUnitaryMatrix(): use
getExponentValue() to retrieve the exponent and check that it matches one of the
supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons),
ensuring the predicate only returns true when getUnitaryMatrix() can actually
produce a result.
- Around line 621-686: The PowOp::verify() method currently only rejects
duplicate unitary operations but allows non-unitary quantum side effects like
alloc, measure, reset, and qtensor mutations to exist in the body region before
the unitary. Strengthen the verification loop that starts with the ++iter
iterator (which currently checks for extra UnitaryOpInterface operations) to
also reject any non-unitary quantum side effects in the body, mirroring the
approach used in InvOp. Additionally, add validation to reject duplicate input
qubits by ensuring each block argument used by the body unitary is unique and
matches the corresponding target argument without duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c

📥 Commits

Reviewing files that changed from the base of the PR and between eadac62 and ad89dd9.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qc_programs.cpp
🛑 Comments failed to post (6)
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h (1)

552-553: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix inconsistent ctrl MLIR example syntax.

This example is missing the targets keyword (qc.ctrl(%q0) (%a0 = %q1)), while the updated API/docs elsewhere use targets(...). Please align it to prevent copy/paste confusion.

Suggested doc fix
- * qc.ctrl(%q0) (%a0 = %q1) {                                                \
+ * qc.ctrl(%q0) targets(%a0 = %q1) {                                         \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

   * qc.ctrl(%q0) targets(%a0 = %q1) {                                         \
   *   qc.OP_NAME(%PARAM1, %PARAM2) %a0 : !qc.qubit                            \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h` around lines 552 -
553, The example documentation for the `qc.ctrl` operation in the
QCProgramBuilder.h file is missing the `targets` keyword in the syntax. Update
the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include the
`targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align with
the current API documentation and prevent confusion for users copying the
example.
mlir/include/mlir/Dialect/Utils/Utils.h (1)

251-257: 🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="$(fd -p 'Utils.h' mlir/include | head -n1)"
echo "Inspecting: $FILE"
nl -ba "$FILE" | sed -n '245,262p'

echo
echo "Searching for similar unchecked std::next patterns:"
rg -n --type=cpp 'std::next\([^,]+,\s*static_cast<std::ptrdiff_t>\(i\)\)' mlir/include/mlir/Dialect

Repository: munich-quantum-toolkit/core

Length of output: 264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the correct file
echo "=== Finding Utils.h in mlir/include/mlir/Dialect/Utils ==="
find mlir/include/mlir/Dialect/Utils -name "Utils.h" 2>/dev/null || echo "File not found with find"

echo
echo "=== Listing files in mlir/include/mlir/Dialect/Utils ==="
ls -la mlir/include/mlir/Dialect/Utils/ 2>/dev/null || echo "Directory not found"

echo
echo "=== Using git ls-files to find Utils.h ==="
git ls-files | grep -E "mlir/include.*Utils\.h$" | head -20

Repository: munich-quantum-toolkit/core

Length of output: 762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Once we find the correct file, read lines 245-262
FILE="mlir/include/mlir/Dialect/Utils/Utils.h"
if [ -f "$FILE" ]; then
  echo "=== Content of $FILE (lines 245-262) ==="
  cat -n "$FILE" | sed -n '245,262p'
  
  echo
  echo "=== Full context around getBodyUnitary function ==="
  cat -n "$FILE" | sed -n '240,270p'
else
  echo "File $FILE not found"
fi

Repository: munich-quantum-toolkit/core

Length of output: 2538


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for std::next usage with iterator patterns
echo "=== Searching for std::next patterns in MLIR ==="
rg -n 'std::next' mlir/include/mlir/Dialect/Utils/ --type=cpp --type=hpp 2>/dev/null || echo "No matches found"

echo
echo "=== Checking for llvm::make_filter_range usage ==="
rg -n 'make_filter_range' mlir/include/ --type=cpp --type=hpp -A 3 -B 1 | head -50

Repository: munich-quantum-toolkit/core

Length of output: 311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mlir/include/mlir/Dialect/Utils/Utils.h"

echo "=== Checking header guard ==="
head -5 "$FILE"

echo
echo "=== Checking for `#pragma` once ==="
grep -n "pragma once" "$FILE" || echo "No `#pragma` once found"

echo
echo "=== Verifying surrounding code context ==="
cat -n "$FILE" | sed -n '1,20p'

Repository: munich-quantum-toolkit/core

Length of output: 1095


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for similar std::next patterns that might have the same issue
echo "=== Searching for std::next with distance in entire mlir/include ==="
rg -n 'std::next\s*\(' mlir/include --type cpp -A 1 -B 1 | head -80

echo
echo "=== Checking LLVM FilterRange in LLVM headers ==="
find mlir -name "*.h" -o -name "*.hpp" | xargs grep -l "make_filter_range" 2>/dev/null | head -5

Repository: munich-quantum-toolkit/core

Length of output: 679


🌐 Web query:

std::next iterator undefined behavior C++ standard forward iterator bounds checking

💡 Result:

In C++, std::next is defined in terms of std::advance [1][2]. The C++ standard does not require std::next to perform bounds checking [3][4]. Consequently, if you use std::next to advance an iterator beyond the valid range (e.g., past the end of a container), it results in undefined behavior [3][4]. The standard library's iterator operations assume that the caller ensures the resulting iterator remains within the bounds of a valid range (specifically, that every iterator between the start and the destination, inclusive, is non-singular and valid) [5][4]. Because there is no internal bounds checking, std::next will simply perform the underlying increment operations, leading to undefined behavior if those operations violate iterator requirements [3][4]. While the C++ standard does not provide bounds checking for std::next, some specific implementations or compilation modes may provide optional "checked" iterators that trigger runtime assertions or errors in debug builds [6][7]. However, these are not guaranteed by the standard and typically do not exist in release builds to maintain performance [7]. If you need to safely advance an iterator while respecting container bounds, you should either: 1. Check the distance manually (e.g., using std::distance or container size) before calling std::next [3]. 2. Use C++20's std::ranges::next, which provides overloads that accept a sentinel (such as end) to safely bound the advancement [8][9]. This allows you to advance an iterator without exceeding the specified limit [8][10].

Citations:


Avoid undefined iterator advancement in getBodyUnitary.

At line 254, std::next(unitaries.begin(), i) can advance past end() before the bounds check executes, resulting in undefined behavior. Per the C++ standard, std::next performs no bounds checking—advancing an iterator beyond its valid range is undefined behavior regardless of subsequent condition checks.

🔧 Proposed fix
 template <typename UnitaryInterface>
 [[nodiscard]] UnitaryInterface getBodyUnitary(Block& block, size_t i) {
   auto unitaries = llvm::make_filter_range(
       block, [](Operation& op) { return isa<UnitaryInterface>(op); });
-  auto it = std::next(unitaries.begin(), static_cast<std::ptrdiff_t>(i));
+  auto it = unitaries.begin();
+  for (size_t n = 0; n < i && it != unitaries.end(); ++n) {
+    ++it;
+  }
   if (it == unitaries.end()) {
     llvm::reportFatalUsageError("Unitary index out of bounds");
   }
   return cast<UnitaryInterface>(*it);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/include/mlir/Dialect/Utils/Utils.h` around lines 251 - 257, The function
getBodyUnitary calls std::next to advance an iterator by i positions before
checking bounds, which results in undefined behavior since std::next performs no
bounds validation. To fix this, manually iterate through the filtered unitaries
range while counting elements, checking if the count reaches i and if iteration
completes without reaching the target index. Return the unitary operation when
the count matches i, or call llvm::reportFatalUsageError if iteration completes
without finding the i-th unitary.
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)

127-138: 🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, verify the file exists and read the function at the specified lines
if [ -f "mlir/lib/Conversion/QCOToQC/QCOToQC.cpp" ]; then
  echo "=== File found ==="
  wc -l mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  echo ""
  echo "=== Function inlineRegion (lines 127-138) ==="
  sed -n '127,145p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
else
  echo "File not found"
fi

Repository: munich-quantum-toolkit/core

Length of output: 1082


🏁 Script executed:

#!/bin/bash
# Search for the moveRegion function mentioned in the diff
echo "=== Searching for moveRegion ==="
fd -t f "\.cpp$|\.h$" mlir/ | xargs rg "moveRegion" -B 2 -A 5 | head -50

Repository: munich-quantum-toolkit/core

Length of output: 3926


🏁 Script executed:

#!/bin/bash
# Find the other call sites mentioned (821-823, 867-871, 912-919)
echo "=== Checking call sites in the file ==="
sed -n '821,823p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
sed -n '867,871p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
sed -n '912,919p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1082


🏁 Script executed:

#!/bin/bash
# Search for all calls to inlineRegion in the file
echo "=== All calls to inlineRegion ==="
rg "inlineRegion\s*\(" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 2 -A 2

Repository: munich-quantum-toolkit/core

Length of output: 1373


🏁 Script executed:

#!/bin/bash
# Search for moveRegion in QCOToQC.cpp specifically
echo "=== Searching for moveRegion in QCOToQC.cpp ==="
rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo "Exit code: $?"
echo ""

# Check the full context around inlineRegion calls to understand type conversion
echo "=== Checking context around line 821 ==="
sed -n '810,830p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""

# Check class context - see if this is a pattern rewriter
echo "=== Searching for class context and pattern definition ==="
sed -n '1,50p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 2527


🏁 Script executed:

#!/bin/bash
# Look for type conversion or adaptor usage in the region operations
echo "=== Searching for adaptor usage around inlineRegion calls ==="
rg -B 5 "inlineRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | head -80

Repository: munich-quantum-toolkit/core

Length of output: 1985


🏁 Script executed:

#!/bin/bash
# Check the QCOToJeff moveRegion implementation to understand the pattern
echo "=== Full moveRegion implementation in QCOToJeff.cpp ==="
rg "static LogicalResult moveRegion" mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp -A 30

Repository: munich-quantum-toolkit/core

Length of output: 1421


🏁 Script executed:

#!/bin/bash
# Check if there's any type conversion or adaptor in QCOToQC patterns
echo "=== Searching for TypeConverter usage in QCOToQC.cpp ==="
rg "TypeConverter\|getTypeConverter" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Find where moveRegion is called in QCOToQC.cpp
echo "=== Full context of moveRegion calls in QCOToQC.cpp ==="
rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 10 -A 3

Repository: munich-quantum-toolkit/core

Length of output: 1306


🏁 Script executed:

#!/bin/bash
# Check if moveRegion is defined in QCOToQC.cpp
echo "=== Search for moveRegion definition ==="
rg "static.*moveRegion\|LogicalResult.*moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 15

Repository: munich-quantum-toolkit/core

Length of output: 206


🏁 Script executed:

#!/bin/bash
# Look at the complete ForOp pattern to understand type conversion concern
echo "=== Complete ForOp pattern class ==="
sed -n '809,850p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1554


🏁 Script executed:

#!/bin/bash
# Check if there's any type conversion happening with adaptor values
echo "=== Adaptor type context in patterns ==="
rg "struct.*Adaptor\|class.*Adaptor\|OpAdaptor" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 5 | head -50

Repository: munich-quantum-toolkit/core

Length of output: 206


🏁 Script executed:

#!/bin/bash
# Check the return type and error handling in current inlineRegion to see if it's supposed to fail
echo "=== Checking if void return is problematic ==="
sed -n '120,160p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1959


🏁 Script executed:

#!/bin/bash
# Find all function definitions in QCOToQC.cpp
echo "=== All function definitions ==="
ast-grep outline mlir/lib/Conversion/QCOToQC/QCOToQC.cpp --view expanded | grep -A 2 "moveRegion\|inlineRegion"

Repository: munich-quantum-toolkit/core

Length of output: 198


🏁 Script executed:

#!/bin/bash
# Search more broadly for moveRegion in the file
echo "=== All occurrences of moveRegion with context ==="
grep -n "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 364


🏁 Script executed:

#!/bin/bash
# Check line counts around the moveRegion usages - are they before or after inlineRegion?
echo "=== Lines in QCOToQC.cpp ==="
wc -l mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== Lines with moveRegion ==="
grep -n "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== Lines with inlineRegion ==="
grep -n "inlineRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 979


🏁 Script executed:

#!/bin/bash
# Check if moveRegion is a helper lambda or a separate function
echo "=== Context around first moveRegion call (around line 1001) ==="
sed -n '995,1005p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 840


🏁 Script executed:

#!/bin/bash
# Look for any function that might be moveRegion before the first call
echo "=== Search in the area before first moveRegion call ==="
sed -n '100,200p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -A 5 -B 5 "moveRegion\|inlineRegion"

Repository: munich-quantum-toolkit/core

Length of output: 1097


🏁 Script executed:

#!/bin/bash
# Look at the moveRegion calls with full context
echo "=== moveRegion call at line 649 ==="
sed -n '640,660p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== moveRegion call at line 687 ==="
sed -n '678,698p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1537


🏁 Script executed:

#!/bin/bash
# Find the moveRegion function definition - it must be defined somewhere
echo "=== Search for moveRegion definition in entire file ==="
sed -n '1,700p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -B 5 -A 30 "moveRegion"

Repository: munich-quantum-toolkit/core

Length of output: 1756


🏁 Script executed:

#!/bin/bash
# Check if it's imported from a header
echo "=== Check headers for moveRegion ==="
head -50 mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -i "include"
echo ""
echo "=== Search for moveRegion in header files ==="
find mlir/lib/Conversion -name "*.h" -exec grep -l "moveRegion" {} \;

Repository: munich-quantum-toolkit/core

Length of output: 1282


🏁 Script executed:

#!/bin/bash
# Let's look at the full section from line 640-700 to see the complete pattern
echo "=== Complete pattern around moveRegion calls ==="
sed -n '630,700p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 2146


🏁 Script executed:

#!/bin/bash
# Search for moveRegion definition anywhere in the codebase
echo "=== Search for moveRegion definition across all files ==="
rg "^\s*auto moveRegion\s*=|^\s*static.*LogicalResult moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 20

Repository: munich-quantum-toolkit/core

Length of output: 223


🏁 Script executed:

#!/bin/bash
# Check if moveRegion might be a lambda defined in the pass structure
echo "=== Check the full pass structure ==="
sed -n '1040,1098p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 2742


🏁 Script executed:

#!/antml:bash
# Search backwards from first moveRegion call to find definition
echo "=== Search in lines 1-648 for moveRegion definition ==="
sed -n '1,648p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | tail -100 | grep -B 20 -A 5 "moveRegion\|^static"

Repository: munich-quantum-toolkit/core

Length of output: 221


🏁 Script executed:

#!/bin/bash
# Let's check the include file that defines the pass
echo "=== Check QCOToQC.h ==="
find mlir -name "QCOToQC.h" -exec cat {} \;

Repository: munich-quantum-toolkit/core

Length of output: 660


🏁 Script executed:

#!/bin/bash
# Search more carefully - moveRegion might be defined as a member function of the pass
echo "=== Full context of the pass class definition ==="
sed -n '950,1098p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | head -150

Repository: munich-quantum-toolkit/core

Length of output: 6008


🏁 Script executed:

#!/bin/bash
# Search the entire repository for moveRegion definition
echo "=== All moveRegion definitions in the repo ==="
rg "static.*LogicalResult.*moveRegion\|auto\s+moveRegion\s*=" --type cpp --type h -B 2 -A 15

Repository: munich-quantum-toolkit/core

Length of output: 212


🏁 Script executed:

#!/antml:bash
# Check around the ConvertQCOCtrlOp and ConvertQCOInvOp to understand what moveRegion they're calling
echo "=== Full ConvertQCOCtrlOp struct ==="
sed -n '628,666p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1303


🏁 Script executed:

#!/bin/bash
# Check if this conversion is actually compiling - moveRegion must be defined somewhere
echo "=== Check if file builds - look for compilation artifacts ==="
find . -name "CMakeLists.txt" -path "*/QCOToQC*" -exec cat {} \;

Repository: munich-quantum-toolkit/core

Length of output: 1922


🏁 Script executed:

#!/bin/bash
# Search all files in Conversion directory for moveRegion
echo "=== All references to moveRegion in Conversion directory ==="
rg "moveRegion" mlir/lib/Conversion/ --type cpp --type h | head -30

Repository: munich-quantum-toolkit/core

Length of output: 1595


🏁 Script executed:

#!/bin/bash
# This is key - moveRegion is called in QCOToQC.cpp but not defined. Let's verify this is a real issue
echo "=== Check if QCOToQC.cpp compiles or if moveRegion is missing ==="
rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 5 -A 2 | head -60
echo ""
echo "=== Check for static moveRegion in QCOToQC.cpp ==="
rg "static.*moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo "Exit code: $?"
echo ""
echo "=== Check all helper functions defined in QCOToQC.cpp before the patterns ==="
sed -n '100,650p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -B 2 -A 10 "^static\|^auto"

Repository: munich-quantum-toolkit/core

Length of output: 2299


🏁 Script executed:

#!/bin/bash
# Check QCToQCO.cpp to see if it has the same issue
echo "=== Check QCToQCO.cpp for moveRegion definition ==="
rg "static.*moveRegion\|LogicalResult moveRegion" mlir/lib/Conversion/QCToQCO/QCToQCO.cpp -A 15

Repository: munich-quantum-toolkit/core

Length of output: 217


🏁 Script executed:

#!/bin/bash
# Let's check the ConversionUtils header to see if moveRegion is defined there
echo "=== Check ConversionUtils.h for moveRegion ==="
find mlir -name "ConversionUtils.h" -exec cat {} \; | grep -B 2 -A 15 "moveRegion"

Repository: munich-quantum-toolkit/core

Length of output: 851


🏁 Script executed:

#!/bin/bash
# Check if there's a common header that defines moveRegion
echo "=== Check all includes in QCOToQC.cpp that might have moveRegion ==="
head -40 mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== Look for moveRegion in those header files ==="
find mlir/Conversion -name "*.h" | xargs grep -l "moveRegion" 2>/dev/null

Repository: munich-quantum-toolkit/core

Length of output: 1589


🏁 Script executed:

#!/bin/bash
# Get the full ConversionUtils.h definition to understand moveRegion
echo "=== Full ConversionUtils.h content ==="
find mlir -name "ConversionUtils.h" -type f -exec cat {} \;

Repository: munich-quantum-toolkit/core

Length of output: 1910


🏁 Script executed:

#!/bin/bash
# Now verify the concern - check if SCF patterns using inlineRegion with adaptor values
echo "=== Check ForOp pattern with adaptor.getInitArgs() ==="
sed -n '809,835p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== Check WhileOp pattern with adaptor values ==="
sed -n '854,876p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== Check IfOp pattern with adaptor values ==="
sed -n '900,925p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 3098


🏁 Script executed:

#!/bin/bash
# Verify that CtrlOp and InvOp already use moveRegion successfully
echo "=== CtrlOp using moveRegion ==="
sed -n '641,664p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "=== InvOp using moveRegion ==="
sed -n '679,702p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1535


🏁 Script executed:

#!/bin/bash
# Verify that adaptor values are indeed type-converted
# Check how OpAdaptor works with type conversion in MLIR patterns
echo "=== Understanding the issue: adaptor provides converted types ==="
echo "In SCF patterns:"
echo "- op.getInitArgs() returns QCO-typed values (original operation)"
echo "- adaptor.getInitArgs() returns QC-typed values (type-converted)"
echo ""
echo "When inlineRegion is called with adaptor values:"
echo "- Block args in inlined region still have QCO types"
echo "- Then they're replaced with QC-typed adaptor values"
echo "- This violates MLIR's same-type RAUW invariant"
echo ""
echo "=== Confirm the pattern is used correctly elsewhere ==="
rg "inlineRegion\|moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 3 | grep -A 3 "inlineRegion\|moveRegion" | head -100

Repository: munich-quantum-toolkit/core

Length of output: 638


🏁 Script executed:

#!/bin/bash
# Check if there are any compile errors or test failures related to this
echo "=== Check test file to understand expected behavior ==="
find mlir -path "*/test*" -name "*qco_to_qc*" -type f | head -5

Repository: munich-quantum-toolkit/core

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Verify return type consistency - inlineRegion is void but moveRegion returns LogicalResult
echo "=== Function signatures comparison ==="
echo "inlineRegion signature:"
sed -n '127,129p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
echo ""
echo "moveRegion signature (from ConversionUtils.h):"
echo "inline LogicalResult moveRegion(...)"
echo ""
echo "This means the proposed fix needs to:"
echo "1. Change inlineRegion to return LogicalResult"
echo "2. Update all callers to handle the return value"

Repository: munich-quantum-toolkit/core

Length of output: 657


Update SCF region inline patterns to convert block argument types before RAUW.

inlineRegion (lines 127-138) inlines regions with original block-argument types, then replaces those args using type-converted adaptor values. This violates MLIR's same-type RAUW invariant when adaptor values have different types (e.g., QC-typed) than the block arguments (e.g., QCO-typed).

The SCF conversion patterns (ForOp at 822, WhileOp at 868-870, IfOp at 913-918) pass type-converted adaptor values to inlineRegion, triggering this issue. However, the QCO control-flow patterns (CtrlOp, InvOp) correctly use moveRegion from ConversionUtils.h to convert block signatures during the inline operation, avoiding the invariant violation.

Change inlineRegion to use the type converter to convert block argument signatures before argument replacement, similar to the moveRegion pattern already in use. Update the function signature to return LogicalResult and modify all call sites to handle the result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 127 - 138, The
inlineRegion function currently inlines regions and then attempts to replace
block arguments with type-converted adaptor values, violating MLIR's same-type
RAUW invariant when the adaptor values have different types than the original
block arguments. Modify inlineRegion to use the type converter to convert the
block argument types before performing the replaceAllUsesWith operation,
following the pattern used by moveRegion from ConversionUtils.h. Change the
function signature to return LogicalResult instead of void to properly
communicate conversion failures. Update all call sites of inlineRegion (in the
ForOp pattern around line 822, WhileOp pattern around lines 868-870, and IfOp
pattern around lines 913-918) to handle the returned LogicalResult.
mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp (3)

343-355: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve parent modifier validity when erasing empty inv.

Line 354 removes the InvOp even when it is the sole body unitary of another modifier, leaving the parent body with only qco.yield. Use an IdOp for the single-target nested case and bail for multi-target nested identity, matching the pow(0) handling.

🛡️ Suggested nested-modifier guard
-    rewriter.replaceOp(op, op.getOperands());
+    if (isa<CtrlOp, InvOp, PowOp>(op->getParentOp())) {
+      if (op.getNumTargets() != 1) {
+        return failure();
+      }
+      rewriter.replaceOpWithNewOp<IdOp>(op, op.getInputQubit(0));
+    } else {
+      rewriter.replaceOp(op, op.getOperands());
+    }
     return success();
   }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 343 - 355, The
EraseEmptyInv struct's matchAndRewrite method removes InvOp operations without
checking if they are nested as the sole body unitary within another modifier,
which violates modifier body constraints. Before the current check on
op.getNumBodyUnitaries() in matchAndRewrite, add logic to detect if the InvOp is
nested inside a parent modifier body and handle it accordingly: for
single-target nested cases, replace with an IdOp instead of removing it; for
multi-target nested cases, return failure to prevent the rewrite. This approach
should mirror the handling used in the pow(0) pattern to maintain parent
modifier validity.

403-430: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Require a YieldOp before reading terminator operands.

Line 426 dereferences block.getTerminator() without proving the body is non-empty and yield-terminated. Malformed IR should get a verifier diagnostic instead of a null dereference or a non-yield “terminator” being accepted.

🛠️ Suggested verifier guard
-  auto* blockTerminator = block.getTerminator();
-  if (const auto numYieldOperands = blockTerminator->getNumOperands();
+  if (block.empty() || !isa<YieldOp>(block.back())) {
+    return emitOpError(
+        "last operation in body region must be a yield operation");
+  }
+  auto yieldOp = cast<YieldOp>(block.back());
+  if (const auto numYieldOperands = yieldOp->getNumOperands();
       numYieldOperands != numTargets) {
     return emitOpError("yield operation must yield ")
            << numTargets << " values, but found " << numYieldOperands;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 403 - 430, The
InvOp::verify() method dereferences block.getTerminator() without first
validating that the body is non-empty and that the terminator is specifically a
YieldOp. Before line 426 where getTerminator() is called, add a verification
check to ensure the terminator exists and is a YieldOp instance, returning an
appropriate emitOpError if either condition fails. This will prevent null
dereferences and reject malformed IR that contains non-yield terminators.

448-517: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Constrain matrix availability to the cases getUnitaryMatrix() implements.

Line 449 returns true for multi-target bodies with multiple known unitaries, but lines 514-515 return std::nullopt for those cases. Include the same zero/sole-unitary/single-target-composition conditions in the predicate.

🛠️ Minimal predicate alignment
 bool InvOp::hasCompileTimeKnownUnitaryMatrix() {
+  if (getNumBodyUnitaries() == 0) {
+    return true;
+  }
+  if (utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody())) {
+    return all_of(getBody()->getOps<UnitaryOpInterface>(),
+                  [](UnitaryOpInterface op) {
+                    return op.hasCompileTimeKnownUnitaryMatrix();
+                  });
+  }
+  if (getNumTargets() != 1) {
+    return false;
+  }
   return all_of(getBody()->getOps<UnitaryOpInterface>(),
                 [](UnitaryOpInterface op) {
                   return op.hasCompileTimeKnownUnitaryMatrix();
                 });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 448 - 517, The
hasCompileTimeKnownUnitaryMatrix() function returns true for cases where
getUnitaryMatrix() returns std::nullopt, causing inconsistency. Modify the
hasCompileTimeKnownUnitaryMatrix() predicate to include the same conditional
checks that getUnitaryMatrix() implements: it should verify that
getNumBodyUnitaries() returns zero, or that there is a sole body unitary
available, or that getNumTargets() equals one (for the composition case). This
ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔁 MLIR - pow(r) modifier (integer r first; floats deferred) 🔁 MLIR - Gate modifiers framework

3 participants